﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;

namespace Xunit.Sdk
{
	/// <summary>
	/// The test invoker for xUnit.net v2 tests.
	/// </summary>
	public class XunitTestInvoker : TestInvoker<IXunitTestCase>
	{
#if NET_4_0_ABOVE
		private readonly IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes;
#else
		private readonly IList<BeforeAfterTestAttribute> beforeAfterAttributes;
#endif
		private readonly Stack<BeforeAfterTestAttribute> beforeAfterAttributesRun = new Stack<BeforeAfterTestAttribute>();

		/// <summary>
		/// Initializes a new instance of the <see cref="XunitTestInvoker"/> class.
		/// </summary>
		/// <param name="test">The test that this invocation belongs to.</param>
		/// <param name="messageBus">The message bus to report run status to.</param>
		/// <param name="testClass">The test class that the test method belongs to.</param>
		/// <param name="constructorArguments">The arguments to be passed to the test class constructor.</param>
		/// <param name="testMethod">The test method that will be invoked.</param>
		/// <param name="testMethodArguments">The arguments to be passed to the test method.</param>
		/// <param name="beforeAfterAttributes">The list of <see cref="BeforeAfterTestAttribute"/>s for this test invocation.</param>
		/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
		/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
		public XunitTestInvoker(ITest test,
														IMessageBus messageBus,
														Type testClass,
														object[] constructorArguments,
														MethodInfo testMethod,
														object[] testMethodArguments,
#if NET_4_0_ABOVE
 IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
#else
 IList<BeforeAfterTestAttribute> beforeAfterAttributes,
#endif
 ExceptionAggregator aggregator,
														CancellationTokenSource cancellationTokenSource)
			: base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, aggregator, cancellationTokenSource)
		{
			this.beforeAfterAttributes = beforeAfterAttributes;
		}

		/// <summary>
		/// Gets the list of <see cref="BeforeAfterTestAttribute"/>s for this test invocation.
		/// </summary>
#if NET_4_0_ABOVE

		protected IReadOnlyList<BeforeAfterTestAttribute> BeforeAfterAttributes
#else

		protected IList<BeforeAfterTestAttribute> BeforeAfterAttributes
#endif
		{
			get { return beforeAfterAttributes; }
		}

		/// <inheritdoc/>
		protected override Task BeforeTestMethodInvokedAsync()
		{
			foreach (var beforeAfterAttribute in beforeAfterAttributes)
			{
				var attributeName = beforeAfterAttribute.GetType().Name;
				if (!MessageBus.QueueMessage(new BeforeTestStarting(Test, attributeName)))
					CancellationTokenSource.Cancel();
				else
				{
					try
					{
						Timer.Aggregate(() => beforeAfterAttribute.Before(TestMethod));
						beforeAfterAttributesRun.Push(beforeAfterAttribute);
					}
					catch (Exception ex)
					{
						Aggregator.Add(ex);
						break;
					}
					finally
					{
						if (!MessageBus.QueueMessage(new BeforeTestFinished(Test, attributeName)))
							CancellationTokenSource.Cancel();
					}
				}

				if (CancellationTokenSource.IsCancellationRequested)
					break;
			}

#if NET_4_0_ABOVE
			return Task.FromResult(0);
#else
			return TaskEx.FromResult(0);
#endif
		}

		/// <inheritdoc/>
		protected override Task AfterTestMethodInvokedAsync()
		{
			foreach (var beforeAfterAttribute in beforeAfterAttributesRun)
			{
				var attributeName = beforeAfterAttribute.GetType().Name;
				if (!MessageBus.QueueMessage(new AfterTestStarting(Test, attributeName)))
					CancellationTokenSource.Cancel();

				Aggregator.Run(() => Timer.Aggregate(() => beforeAfterAttribute.After(TestMethod)));

				if (!MessageBus.QueueMessage(new AfterTestFinished(Test, attributeName)))
					CancellationTokenSource.Cancel();
			}

#if NET_4_0_ABOVE
			return Task.FromResult(0);
#else
			return TaskEx.FromResult(0);
#endif
		}
	}
}